sCanvas.fn.extend(true, {

	//stores all canvas objects as map (id: object)
	objects: { },

	//stores bottom canvas object
	bottom: null,

	//stores top canvas object
	top: null,

	//stores default attributes values of newly created objects
	defaultAttrs: {
		x: 0,
		y: 0,
		rotation: 0,
		id: null,
		group: [ ],
		visible: true,
		stroke: null,
		fill: "black",
		originX: 0,
		originY: 0,
		lineWidth: 1,
		lineCap: "butt",
		lineJoin: "miter",
		miterLimit: 10,
		opacity: 1,
		shadowColor: "rgba(0, 0, 0, 0)",
		shadowBlur: 0,
		shadowX: 0,
		shadowY: 0
	},

	//performs all common tasks while creating new object
	register: function(obj) {
		//if registering first object
		if (this.top !== null) {
//			this.top.over(obj);
			this.top.overObj = obj;
			obj.underObj = this.top;
			this.top = obj;
		} else {
			this.top = obj;
			this.bottom = obj;
		}
	},

	//returns canvas object with passed id
	get: function(id) {
		if (this.objects[id] !== undefined) {
			return this.objects[id];
		} else {
			return this.nullObject();
		}
	},

	//adds object with id
	addIdObject: function(obj, id) {
		this.objects[id] = obj;
	},

	//changes objects id
	changeIdObject: function(oldId, newId) {
		this.objects[newId] = this.objects[oldId];
		delete this.objects[oldId];
	},

	//remove object with id
	removeIdObject: function(id) {
		delete this.objects[id];
	}
	
});



var 
	//parent of all canvas objects
	sCanvasObject = function() {
		return this;
	};

sCanvas.extend(sCanvasObject.prototype, {

	//stores sCanvas reference
	sc: null,

	//stores object attributes
	attrs: null,

	//stores actual relative object origin position in pixels
	origin: null,

	//stores supported attributes
	supportedAttrs: null,

	//stores supported attributes
	animatableAttrs: null,

	//stores handler binded to attribute change
	attrHandlers: null,

	//stores reference to the over object
	overObj: null,

	//stores reference to the under object
	underObj: null,

	//stores animation queue
	animations: null,

	//stores event handlers
	handlers: null,
	
	//stores data associated with object
	storedData: null,

	//initializes object
	init: function(sc) {
		this.sc = sc;

		this.handlers = {
			mousemove: [ ],
			mouseenter: [ ],
			mouseleave: [ ],
			mouseup: [ ],
			mousedown: [ ],
			click: [ ],
			dblclick: [ ]
		};

		this.supportedAttrs = (
			"x y rotation originX originY"
			+ " fill stroke id group"
			+ " lineWidth lineCap lineJoin miterLimit"
			+ " shadowColor shadowBlur shadowX shadowY"
			+ " visible opacity"
		).split(" ");

		this.animatableAttrs = (
			"x y rotation shadowBlur shadowX shadowY opacity lineWidth"
		).split(" ");

		this.attrHandlers = {
			id: this.setId,
			group: this.setGroup,
			originX: this.setOrigin,
			originY: this.setOrigin
//			rotation: this.setRotation
		};

		this.origin = {
			x: 0,
			y: 0
		};

		this.animations = new AnimationQueue();
	},

	//returns true if object is null object
	isNull: function() {
		return (this instanceof CanvasNull);
	},

	//makes object visible
	show: function() {
		if (this.isNull()) {
			return this;
		}

		this.attr("visible", true);

		return this;
	},

	//makes object invisible
	hide: function() {
		if (this.isNull()) {
			return this;
		}

		this.attr("visible", false);

		return this;
	},

	//returns true if object is visible
	isVisible: function() {
		return !!this.attrs.visible;
	},

	//creates path for drawing filled object
	createFillPath: function(cx) {
		throw new Error("Method sCanvasObject.createFillPath() should be overridden!");
	},

	//creates path for drawing object stroke
	createStrokePath: function(cx) {
		throw new Error("Method sCanvasObject.createStrokePath() should be overridden!");
	},

	//creates path for checking if point is in object
	createPointInPath: function(cx) {
		throw new Error("Method sCanvasObject.createPointInPath() should be overridden!");
	},

	//returns object containing info about the most top, right, bottom and left point of object
	box: function() {
		throw new Error("Method sCanvasObject.box() should be overridden!");
	},
	
	//returns rotated center poínt
	rotatedCenter: function() {
		var x, y, r, w, a;
		
		//if origin was not moved
		if (this.origin.x === 0 && this.origin.y === 0) {
			return {
				x: this.attrs.x,
				y: this.attrs.y
			};
		} else {
			x = this.origin.x; //x-axis distance
			y = this.origin.y; //y-axis distance
			r = Math.sqrt(x * x + y * y); //absolute distance
			
			w = Math.atan(y / x);
			
			//get angle between rotated r and x-axis
			if (x >= 0) {
				a = Math.PI + w + this.attrs.rotation;
			} else {
				if (y >= 0) {
					a = w + this.attrs.rotation;
				} else {
					a = 2 * Math.PI + w + this.attrs.rotation;
				}
			}
			
			return {
				x: this.attrs.x + r * Math.cos(a),
				y: this.attrs.y + r * Math.sin(a)
			};
		}
	},

	//draws object on canvas
	draw: function(cx) {
		if (this.isVisible()) {
			cx.shadowColor = this.attrs.shadowColor;
			cx.shadowBlur = this.attrs.shadowBlur;
			cx.shadowOffsetX = this.attrs.shadowX;
			cx.shadowOffsetY = this.attrs.shadowY;
			cx.globalAlpha = this.attrs.opacity;
			if (this.attrs.fill !== null) {
				this.createFillPath(cx);
				cx.fillStyle = this.attrs.fill;
				cx.fill();
			}

			if (this.attrs.stroke !== null && this.attrs.lineWidth > 0) {
				this.createStrokePath(cx);
				cx.strokeStyle = this.attrs.stroke;
				cx.lineWidth = this.attrs.lineWidth;
				cx.lineCap = this.attrs.lineCap;
				cx.lineJoin = this.attrs.lineJoin;
				cx.miterLimit = this.attrs.miterLimit;
				cx.stroke();
			}
		}
	},

	//gets/sets over object
	over: function(obj) {
		if (this.isNull()) {
			return this;
		}

		if (obj === undefined) {
			//getter
			return this.overObj;
		} else {
			//setter

			//update old neighbours of passed object and global stack
			if (obj.overObj !== null) {
				obj.overObj.underObj = obj.underObj;
			} else {
				this.sc.top = obj.underObj;
			}
			
			if (obj.underObj !== null) {
				obj.underObj.overObj = obj.overObj;
			} else {
				this.sc.bottom = obj.overObj;
			}

			//update passed object
			obj.overObj = this.overObj;
			obj.underObj = this;

			//update new neighbours of passed object
			if (this.overObj !== null) {
				this.overObj.underObj = obj;
			} else {
				this.sc.top = obj;
			}
			this.overObj = obj;
			
			this.sc.autorepaint();

			return this;
		}
	},

	//gets/sets under object
	under: function(obj) {
		if (this.isNull()) {
			return this;
		}

		if (obj === undefined) {
			//getter
			return this.underObj;
		} else {
			//setter

			//update old neighbours of passed object and global stack
			if (obj.overObj !== null) {
				obj.overObj.underObj = obj.underObj;
			} else {
				this.sc.top = obj.underObj;
			}
			
			if (obj.underObj !== null) {
				obj.underObj.overObj = obj.overObj;
			} else {
				this.sc.bottom = obj.overObj;
			}

			//update passed object
			obj.underObj = this.underObj;
			obj.overObj = this;

			//update new neighbours of passed object
			if (this.underObj !== null) {
				this.underObj.overObj = obj;
			} else {
				this.sc.bottom = obj;
			}
			this.underObj = obj;

			this.sc.autorepaint();

			return this;
		}
	},

	//adds supported attribute for object
	addSupportedAttr: function(attr) {
		var i;
		if (typeof attr === "string") {
			this.supportedAttrs.push(attr);
		} else {
			for (i = 0; i < attr.length; i++) {
				this.supportedAttrs.push(attr[i]);
			}
		}
	},

	//adds animatable attribute for object
	addAnimatableAttr: function(attr) {
		var i;
		if (typeof attr === "string") {
			this.animatableAttrs.push(attr);
		} else {
			for (i = 0; i < attr.length; i++) {
				this.animatableAttrs.push(attr[i]);
			}
		}
	},

	//initializes attrs with supported attributes and sets them to default value
	initAttrs: function() {
		var attr, i,
			defaults = this.sc.defaultAttrs;

		this.attrs = { };
		for (i = 0; i < this.supportedAttrs.length; i++) {
			attr = this.supportedAttrs[i];
			if (defaults[attr] !== undefined) {
				this.attrs[attr] = defaults[attr];
			} else {
				this.attrs[attr] = null;
			}
		}
		//TODO: handle this nicely
		this.attrs.group = [ ];
	},

	//gets/sets object attribute(s)
	attr: function(name, value) {
		if (this.isNull()) {
			return this;
		}

		var attr, temp;

		//handling one attribute
		if (typeof name === "string") {
			//check if attribute is supported
			if (!this.attrExists(name)) {
				throw new Error("Attribute " + name + " is not supported.");
			}

			if (value === undefined) {
				//getting value
				return this.attrs[name];
			} else {
				//setting value
				this.setAttr(name, value);
				this.sc.autorepaint();
				return this;
			}
		}

		//setting multiple attributes
		//TODO: check if name is map

		//ensure setting x/originY after setting x/y
//			if (name.originX !== undefined) {
//				temp = name.originX;
//				delete name.originX;
//				name.originX = temp;
//			}
//			if (name.originY !== undefined) {
//				temp = name.originY;
//				delete name.originY;
//				name.originY = temp;
//			}

		for (attr in name) {
			if (!this.attrExists(attr)) {
				throw new Error("Attribute " + attr + " is not supported.");
			} else {
				this.setAttr(attr, name[attr]);
			}
		}
		this.sc.autorepaint();
		return this;
	},

	//checks if attribute is supported
	attrExists: function(name) {
		return this.attrs.hasOwnProperty(name);
	},

	//sets attribute value
	setAttr: function(name, value) {
		//if is set handler for attribut change invoke it
		//otherwise just set attribute value
		if (this.attrHandlers[name] !== undefined) {
			this.attrHandlers[name].apply(this, [value, this.attrs[name], name]);
		} else {
			this.attrs[name] = value;
		}
	},

	//invoked when object id is set
	setId: function(id, oldId) {
		if (id === "" || id === null) { //if id is removed
			if (oldId !== null) {
				this.sc.removeIdObject(oldId);
				this.attrs.id = null;
			}
		} else if (sCanvas.isValidId(id)) { //if id is valid
			if (this.sc.get(id).isNull()) { //if id is not already used
				if (oldId !== null) { //if object already has id
					this.sc.changeIdObject(oldId, id);
				} else {
					this.sc.addIdObject(this, id);
				}
				this.attrs.id = id;
			} else {
				throw new Error("Passed ID is already used.");
			}
		} else {
			throw new Error("Passed ID is not valid.");
		}
	},

	//invoked when object group is set
	setGroup: function(group, oldGroup) {
		var i, groups;

		//remove all current groups
		if (oldGroup !== null) {
			for (i = 0; i < oldGroup.length; i++) {
				this.removeGroup(oldGroup[i]);
			}
		}
		this.attrs.group = [ ];

		if (group === "" || group === null) {
			;
		} else if (typeof group === "string") {
			groups = group.split(" ");

			for (i = 0; i < groups.length; i++) {
				if (!groups[i].match(/^\s*$/)) { //if group is not empty string
					this.addGroup(groups[i]);
				}
			}
		} else {
			//TODO: check if passed object is group
			group.add(this);
		}
	},

	//invoked when object origin attribute is set
	setOrigin: function(value, old, origin) {
		var axis;
		if (origin === "originX") {
			axis = "x";
		} else {
			axis = "y";
		}

		if (typeof value === "number") {
			//this.origin[axis] = this.attrs[axis] - old + value;
			this.origin[axis] = value;
		} else if (typeof value === "string") {
			if (value.charAt(0) === "=") {
				//this.origin[axis] = parseFloat(value.slice(1), 10);
				this.origin[axis] = -this.attrs[axis] + old + parseFloat(value.slice(1), 10);
//				} else if (value.charAt(value.length - 1) === "%") {
//					this.origin[axis] = edge + Math.round(parseFloat(value, 10) * size / 100);
			} else {
				//this.origin[axis] = this.attrs[axis] - old + parseFloat(value, 10);
				this.origin[axis] = parseFloat(value, 10);
			}
		} // else do nothing

		//recount x/y attribute so object stays on position
		this.attrs[axis] = this.attrs[axis] - old + this.origin[axis];

		this.attrs[origin] = value;
	},

//	//invoked when object rotation is set
//	setRotation: function(value) {
//		var val;
//		
//		if (typeof value === "string") {
//			if (value.indexOf("rad") !== -1) {
//				val = parseFloat(value, 10);
//			} else {
//				val = parseFloat(value, 10) * Math.PI / 180;
//			}
//		} else {
//			val = +value * Math.PI / 180;
//		}
//		
//		this.attrs.rotation = val;
//	},

	//adds object to group
	addGroup: function(id) {
		this.sc.group(id).add(this);

		return this;
	},

	//removes object from group
	removeGroup: function(id) {
		this.sc.group(id).remove(this);

		return this;
	},

	//returns true if object is in group
	inGroup: function(id) {
		return this.sc.group(id).has(this);
	},

	//adds group to object attribute
	//internal
	group: function(id) {
		if (this.attrs.group === null) {
			this.attrs.group = [ id ];
		} else {
			this.attrs.group.push(id);
		}
	},

	//removes group from object attribute
	//internal
	ungroup: function(id) {
		var i = this.attrs.group.indexOf(id);
		if (i !== -1) {
			this.attrs.group.splice(i, 1);
		}
	},

	//registers object animation
	animate: function(attr, duration, easing, complete) {
		if (this.isNull()) {
			return this;
		}

		var options;

		//unify arguments and make options map
		if (duration === undefined) {
			options = { };
		} else if (typeof duration === "number" || typeof duration === "string") {
			options = {
				duration: duration
			};

			if (typeof easing === "string") {
				options.easing = easing;
			} else if (typeof easing === "function") {
				options.complete = easing;
			}

			if (typeof complete === "function") {
				options.complete = complete;
			}
		} else if (typeof duration === "function") {
			options = {
				complete: duration
			};
		} else {
			options = duration;
		}

		this.addAnimation(new Animation(this, attr, options));

		return this;
	},

	//adds function to animation queue
	queue: function(fn) {
		if (this.isNull()) {
			return this;
		}

		if (typeof fn !== "function") {
			throw new TypeError("Function was not passed.");
		}

		this.addAnimation(new Animation(this, { }, {
			duration: 0,
			complete: fn
		}));

		return this;
	},

	//delay execution of animations in animation queue
	delay: function(duration) {
		if (this.isNull()) {
			return this;
		}

		this.addAnimation(new Animation(this, { }, {
			duration: duration
		}));

		return this;
	},

	//stop currently running animation
	stop: function(clear, jump) {
		if (this.isNull()) {
			return this;
		}

		if (this.animations.isEmpty()) {
			return this;
		}

		var anim = this.animations.peek(),
			time = sCanvas.now();

		if (!!jump) {
			sCanvas.extend(this.attrs, anim.finish());
			if (anim.complete !== null) {
				anim.complete.apply(this, [ ]);
			}
			this.sc.repaint();
		}

		if (!!clear) {
			this.clearQueue();
		}

		if (anim.isRepeated()) {
			anim.repeat(true);
			this.stepAnimation(time);
		} else {
			this.animations.remove();

			if (this.animations.isEmpty()) {
				this.sc.removeAnimatedObject(this);
			} else {
				this.animations.peek().start(time);
				this.stepAnimation(time);
			}
		}

		return this;
	},

	//removes all animations that have not started
	clearQueue: function() {
		if (this.isNull()) {
			return this;
		}

		if (!this.animations.isEmpty()) {
			this.animations.peek().repeats = 1;
			this.animations.removeWaiting();
		}

		return this;
	},

	//immediatelly finishes whole animation queue
	finish: function() {
		if (this.isNull()) {
			return this;
		}

		var anim;

		if (!this.animations.isEmpty()) {
			do {
				anim = this.animations.peek();
				anim.computeAttrs();
				sCanvas.extend(this.attrs, anim.finish());
				if (anim.complete !== null) {
					anim.complete.apply(this, [ ]);
				}
//				this.animations.remove();
				if (anim.isRepeated()) {
					anim.repeat();
				} else {
					this.animations.remove();
				}
			} while (!this.animations.isEmpty());
			this.sc.repaint();
			this.sc.removeAnimatedObject(this);
		}

		return this;
	},

	//pauses running animation
	pause: function() {
		if (this.isNull()) {
			return this;
		}

		if (!this.animations.isEmpty()) {
			this.animations.peek().pause();
		}

		return this;
	},

	//reruns paused animation
	play: function() {
		if (this.isNull()) {
			return this;
		}

		if (!this.animations.isEmpty()) {
			this.animations.peek().play();
		}

		return this;
	},

	//returns true if animation is paused
	isPaused: function() {
		if (!this.animations.isEmpty()) {
			return this.animations.peek().isPaused();
		} else {
			return false;
		}
	},

	//changes object attributes during animation
	stepAnimation: function(time) {
		var anim = this.animations.peek();
		sCanvas.extend(this.attrs, anim.next(time));

		//if animation finished, fire next, or if there is no one, tell it to canvas
		if (anim.isFinished()) {
			if (anim.complete !== null) {
				anim.complete.apply(this, [ ]);
			}

			if (anim.isRepeated()) {
				anim.repeat();
				this.stepAnimation(time);
			} else {
				this.animations.remove();

				if (this.animations.isEmpty()) {
					this.sc.removeAnimatedObject(this);
				} else {
					this.animations.peek().start(anim.ending);
					this.stepAnimation(time);
				}
			}
		}
	},

	//pushes animation to animation queue
	addAnimation: function(anim) {
		this.animations.add(anim);

		//if animation queue was empty before adding new animation - fire it
		if (this.animations.length === 1) {
			anim.start();
			this.sc.addAnimatedObject(this);
		}
	},

	//binds event handler
	bind: function(event, fn) {
		if (this.isNull()) {
			return this;
		}

		if (typeof fn !== "function") {
			throw new TypeError("Event handler was not passed.");
		}

		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		if (this.handlers[event].length === 0) {
			this.sc.addEventObject(this, event);
		}

		this.handlers[event].push(fn);

		return this;
	},
	
	//unbinds event handler(s)
	unbind: function(event, fn) {
		if (this.isNull()) {
			return this;
		}

		var i, empty;
		
		if (arguments.length === 0) {
			//removing all handlers of all event types
			for (i in this.handlers) {
				this.handlers[i] = [ ];
				this.sc.removeEventObject(this, i);
			}
		} else {
			if (this.handlers[event] === undefined) {
				throw new ReferenceError("Passed event is not supported.");
			}
			
			if (arguments.length === 1) {
				//removing all handlers of passed event type
				this.handlers[event] = [ ];
				this.sc.removeEventObject(this, event);
			} else {
				//removing only passed handler
				i = this.handlers[event].indexOf(fn);
				if (i !== -1) {
					delete this.handlers[event][i];
					
					//check if this has been last handler
					empty = true;
					for (i in this.handlers[event]) {
						empty = false;
						break;
					}
					
					if (empty) {
						this.handlers[event] = [ ];
						this.sc.removeEventObject(this, event);
					}
				}
			}
		}
		
		return this;
	},

	//triggers event
	trigger: function(event, data) {
		if (this.isNull()) {
			return this;
		}

		var args, e, i;

		//unify arguments
		if (event instanceof EventObject) {
			e = event;
			event = event.type;
		} else {
			e = sCanvas.Event(event);
		}

		//check if passed event type is supported
		if (this.handlers[event] === undefined) {
			throw new ReferenceError("Passed event is not supported.");
		}

		args = [ e ];
		if (sCanvas.isArray(data)) {
			for (i = 0; i < data.length; i++) {
				args.push(data[i]);
			}
		}

		//execute all binded handlers
		for (i in this.handlers[event]) {
			this.handlers[event][i].apply(this, args);

			//if executing of rest of the handlers was stopped
			if (e.isImmediatePropagationStopped()) {
				break;
			}
		}

		//if remove is set to true, handlers are removed
		if (e.isRemovingHandlers()) {
			this.unbind(event);
		}

		return this;
	},

	//checks if specified point is in object
	isPointIn: function(x, y) {
		var box,
			result = false;
		//TODO: optimize - not to check on every invocation
		//if (typeof this.sc.frontCx.isPointInPath === "function") {
		if (this.sc.support.isPointInPath) {
			this.createPointInPath(this.sc.frontCx);
			result = this.sc.frontCx.isPointInPath(x, y);
		} else {
			//if native isPointInPath is not supported or buggy, use box()
			box = this.box();
			
			if (x >= box.left && x <= box.right && y >= box.top && y <= box.bottom) {
				result = true;
			}
		}
		return result;
	},
	
	//stores data associated with object
	data: function(key, value) {
		if (this.isNull()) {
			return this;
		}
		
		if (arguments.length === 0) {
			//getting all associated data
			
			if (this.storedData === null) {
				return { };
			}
			
			return sCanvas.extend({ }, this.storedData);
		} else if (arguments.length === 1) {
			//getting value
			
			//if no data has been stored yet
			if (this.storedData === null) {
				return undefined;
			}
			
			return this.storedData[key];
		} else {
			//setting value
			if (sCanvas.isValidId(key)) {
				if (this.storedData === null) {
					this.storedData = { };
				}
				this.storedData[key] = value;
			} else {
				throw new Error("Passed key is not valid.");
			}
			
			return this;
		}
	},
	
	//removes data associated with object
	removeData: function(key) {
		if (this.isNull()) {
			return this;
		}
		
		//if no data has been stored yet
		if (this.storedData === null) {
			return this;
		}
		
		if (key === undefined) {
			//removing all data
			this.storedData = null;
		} else {
			//removing data with passed key
			delete this.storedData[key];
			
			//if last piece of data has been removed
			if (sCanvas.isEmpty(this.storedData)) {
				this.storedData = null;
			}
		}
		
		return this;
	},
	
	//returns true if object has associated data with passed key
	//if no key is passed, returns true if any data has been associated
	hasData: function(key) {
		if (this.isNull()) {
			return this;
		}
		
		var result = false;
		
		if (key === undefined) {
			result = this.storedData !== null;
		} else {
			result = this.storedData !== null && this.storedData.hasOwnProperty(key);
		}
		
		return result;
	},
	
	//removes object from canvas
	remove: function() {
		if (this.isNull()) {
			return this;
		}
		
		//update neighbours of removed object and global stack
		if (this.overObj !== null) {
			this.overObj.underObj = this.underObj;
		} else {
			this.sc.top = this.underObj;
		}
		
		if (this.underObj !== null) {
			this.underObj.overObj = this.overObj;
		} else {
			this.sc.bottom = this.overObj;
		}
		
		this.overObj = null;
		this.underObj = null;
		this.attr({
			id: null,
			group: null
		})
		.stop(true)
		.unbind();
		
		//this.sc.repaint();
	}

});

//add event shortcuts
var events = (
	"click dblclick mouseup mousedown"
	+ " mouseenter mousemove mouseleave"
).split(" ");

for (var ei = 0; ei < events.length; ei++) {
	sCanvasObject.prototype[events[ei]] = (function() {
		var type = events[ei];
		return function(fn) {
			return fn === undefined ? this.trigger(type) : this.bind(type, fn);
		};
	}());
}

sCanvasObject.prototype.hover = function(fnIn, fnOut) {
	return this.bind("mouseenter", fnIn).bind("mouseleave", fnOut || fnIn);
};

